﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using T0yK4T.Configuration;
using T0yK4T.Data;
using System.ServiceProcess;

namespace ToyChat.Server
{
    /// <summary>
    /// A "Wrapper" for mongodb - This actually just takes care of starting the service (unless mongoservice is set to false, in which case mongod will be started "standalone")
    /// </summary>
    public class MongoWrapper
    {
        private static ConfigurableValue<string> mongoFile = Config.GetValue<string>("mongofile", new StringConverter(), "mongod.exe");
        private static ConfigurableValue<string> dbPath = Config.GetValue<string>("dbpath", new StringConverter(), string.Format("{0}\\data\\db", Environment.CurrentDirectory));
        private static ConfigurableValue<string> logFile = Config.GetValue<string>("mongologfile", new StringConverter(), "mongodb.log");
        private static ConfigurableValue<int> mongoPort = Config.GetValue<int>("mongoport", new Int32Converter(), 27017);
        private static ConfigurableValue<string> mongoServiceName = Config.GetValue<string>("mongoname", new StringConverter(), "MongoDB");

        /// <summary>
        /// The default mongo arguments, default value: "--install --logpath %lp --port %mp --dbpath %dp"
        /// <para/>
        /// Where:
        /// <para/>
        /// %lp is replaced with the full path to the log file (mongoLogPath + \ + logFile)
        /// <para/>
        /// %mp is replaced with mongoPort
        /// <para/>
        /// %dp is replaced with dbPath
        /// </summary>
        private static ConfigurableValue<string> defaultMongoArgs = Config.GetValue<string>("mongoargs", new StringConverter(), "--install --logpath %lp --port %mp --dbpath %dp");

        private static ConfigurableValue<string> mongoLogPath = Config.GetValue<string>("mongologpath", new StringConverter(), Environment.CurrentDirectory);

        /// <summary>
        /// The "non service args" for mongo, default value: "--quiet --port %mp --dbpath %dp"
        /// <para/>
        /// Where:
        /// <para/>
        /// %mp is replaced with mongoPort
        /// <para/>
        /// %dp is replaced with dbPath
        /// </summary>
        private static ConfigurableValue<string> nonserviceArgs = Config.GetValue<string>("mongononserviceargs", new StringConverter(), "--quiet --port %mp --dbpath %dp");
        private static ConfigurableValue<bool> runAsService = Config.GetValue<bool>("mongoservice", new BooleanConverter(), true);
        private static ConfigurableValue<string> mongoServiceMachineName = Config.GetValue<string>("mongomachine", new StringConverter(), Environment.MachineName);
        private Process mongoProcess = new Process();
        private bool processStarted = false;

        /// <summary>
        /// Initializes a new instance of <see cref="MongoWrapper"/> but does not attempt to start mongod
        /// </summary>
        public MongoWrapper()
        { }

        /// <summary>
        /// Closes the MongoDB process (if it was started through this wrapper)
        /// </summary>
        ~MongoWrapper()
        {
            if (processStarted && this.mongoProcess != null)
            {
                if (!this.mongoProcess.HasExited)
                {
                    this.mongoProcess.CloseMainWindow();
                    if (!this.mongoProcess.WaitForExit(5000))
                        this.mongoProcess.Kill();
                }
            }
            this.mongoProcess = null;
        }

        /// <summary>
        /// Gets a value indicating whether or not MongoDB is considered as being available
        /// </summary>
        public bool MongoAvailable
        {
            get
            {
                if (runAsService)
                {
                    ServiceController controller = this.GetController();
                    if (controller != null && controller.Status == ServiceControllerStatus.StartPending)
                        controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(5d));
                    else
                        return controller != null;

                    return controller.Status == ServiceControllerStatus.Running;
                }
                else
                    return this.processStarted && this.mongoProcess != null && !this.mongoProcess.HasExited;
            }
        }

        private ServiceController GetController()
        {
            ServiceController controller;
            try { controller = ServiceController.GetServices(mongoServiceMachineName).Single((sc) => { return sc.ServiceName == mongoServiceName; }); }
            catch { controller = null; }
            return controller;
        }

        private ProcessStartInfo GetInfo(string mongoExe)
        {
            ProcessStartInfo info = new ProcessStartInfo();
            info.FileName = mongoExe;
            string args;
            if (!runAsService)
                args = nonserviceArgs;
            else
                args = defaultMongoArgs;
            try
            {
                info.Arguments =
                    args.
                    Replace("%lp", string.Format("\"{0}\\{1}\"", mongoLogPath, logFile)).
                    Replace("%mp", mongoPort.ToString()).
                    Replace("%dp", string.Format("\"{0}\"", dbPath)); }
            catch { throw; }
            info.RedirectStandardOutput = true;
            info.UseShellExecute = false;
            info.CreateNoWindow = true;
            return info;
        }

        /// <summary>
        /// Attempts to Start MongoDB on the local machine
        /// <para/>
        /// This method will throw an <see cref="InvalidOperationException"/> if mongo is already available
        /// </summary>
        /// <param name="root">The root directory in which the mongo executable is located</param>
        public void StartMongo(string root)
        {
            if (!Directory.Exists(dbPath))
                Directory.CreateDirectory(dbPath);
            ServiceController controller = this.GetController();
            if (controller == null)
            {
                this.StartProcess(root);
                if (runAsService)
                    controller = this.GetController();
                else
                    return;
            }
            if (controller == null)
                throw new InvalidOperationException("Unable to get ServiceController for MongoDB Service");
            if (runAsService)
            {
                if (controller.Status != ServiceControllerStatus.StartPending && controller.Status != ServiceControllerStatus.Running)
                {
                    try { controller.Start(); }
                    catch
                    {
                        try
                        {
                            this.DeleteService();
                            this.StartProcess(root);
                            controller = this.GetController();
                            controller.Start();
                        }
                        catch { throw; }
                    }
                }
                controller.WaitForStatus(ServiceControllerStatus.Running);
            }
        }

        private void DeleteService()
        {
            Process p = new Process();
            ProcessStartInfo info = new ProcessStartInfo("sc", string.Format("delete {0}", mongoServiceName));
            p.StartInfo = info;
            p.Start();
            p.WaitForExit();
            p.Dispose();
        }

        private void StartProcess(string root)
        {
            string mongoExe;
            if (!File.Exists((mongoExe = Path.Combine(root, mongoFile))))
                throw new FileNotFoundException("Unable to start mongod - file not found");
            mongoProcess.StartInfo = this.GetInfo(mongoExe);
            if (runAsService)
            {
                mongoProcess.Start();
                mongoProcess.WaitForExit();
            }
            else
            {
                mongoProcess.Start();
                this.processStarted = true;
                return;
            }
        }

        ///// <summary>
        ///// Attempts to get an <see cref="T0yK4T.Data.IDataAdapter{T}"/> for the specified type <typeparamref name="T"/>
        ///// </summary>
        ///// <typeparam name="T">The type for which to get an adapter</typeparam>
        ///// <returns>Returns a <see cref="T0yK4T.Data.Mongo.MongoDBAdapter{T}"/></returns>
        //public MongoDBAdapter<T> GetAdapter<T>()
        //{
        //    if (!this.MongoAvailable)
        //        throw new InvalidOperationException("Mongo server is not available");
        //    MongoDBAdapter<T> adapter = new MongoDBAdapter<T>(mongoHost, mongoPort, databaseName);
        //    return adapter;
        //}
    }
}